<?php

namespace mongrove;

use \Exception;
use \MongoDBRef;

/**
 * The ReferenceField represents a reference to another
 * Record which can be located in another Collection or even
 * Mongo database. Resolution of references is a transparent
 * process.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 */
class ReferenceField extends SimpleField {

    protected $type;

    /**
     * Define a new ReferenceField with an optional given default value.
     *
     * @param mixed $default The default value to refer to
     */
    public function __construct($default = null) {
        parent :: __construct();

        if($default !== null) {
            $this->setValue($default);
        }
    }

    /**
     * Set the required type of a referenced object.
     *
     * @param string $type
     *
     * @return ReferenceField
     */
    public function setType($type) {
        $this->type = strtolower($type);
        return $this;
    }

    /**
     * Get the required type of a referenced object.
     *
     * @return string
     */
    public function getType() {
        return $this->type;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.SimpleField::getValue()
     */
    public function getValue() {
        $class = $this->value[Constant :: REF_TYPE];

        if($class === null) {
            return null;
        }

        return $class :: getCollection()->findOneById($this->value[Constant :: REF_ID]);
    }

    /*
     * Simple type checking for set values
     *
     * @param mixed $value
     * @throws \Exception
     */
    protected function checkType($value) {
        if($this->type !== null) {
            if(!is_object($value)) {
                throw new \Exception('Referenced value is not an object');
            }

            if(!($value instanceof Record)) {
                throw new \Exception('Referenced value is not a \mongrove\Record');
            }

            $hierarchy = $value :: getTypeHierarchy();

            if(!in_array($this->type, $hierarchy)) {
                $type = mb_strtolower(get_class($value));
                throw new \Exception("Reference type mismatch, expected '{$this->type}', got '{$type}'");
            }

        }
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.SimpleField::setValueImpl()
     */
    public function setValueImpl($value) {
        $this->checkType($value);

        if($value instanceof Record && $value->getId() !== null) {
            $value = self :: createReference($value :: getCollectionName(), $value->getId(), $value :: getCollection()->getDatabase()->__toString(), $value :: getType());
        }

        if(!self :: isReference($value)) {
            return false;
        }

        $this->value = $value;

        return true;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.SimpleField::hydrate()
     */
    public function hydrate($value) {
        $this->value = $value;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.SimpleField::rewriteQuery()
     */
    public function rewriteQuery(array $partialQuery) {
        foreach($partialQuery as $operator => $value) {
            $this->checkType($value);

            if($value instanceof Record && $value->getId() !== null) {
                $value = self :: createReference($value :: getCollectionName(), $value->getId(), $value :: getCollection()->getDatabase()->__toString(), $value :: getType());
            }

            if(!self :: isReference($value)) {
                unset($partialQuery[$operator]);
                continue;
            }

            $partialQuery[$operator] = $value;
        }

        return $partialQuery;
    }

    /**
     * Create a new Mongo reference, annotated with Mongrove Record type 
     * 
     * @param string $collection
     * @param string $id
     * @param string $db
     * @param string $type
     */
    protected static function createReference($collection, $id, $db, $type) {
        $type = mb_strtolower($type);

        $result = MongoDBRef :: create($collection, $id, $db);
        $result[Constant :: REF_TYPE] = $type;

        return $result;
    }

    /**
     * Validate a Mongo reference and ensure it has been annotated with the Mongrove Record $type
     *
     * @param array $value
     */
    protected static function isReference($value) {
        if(!is_array($value)) {
            return false;
        }

        return MongoDBRef :: isRef($value) && isset($value[Constant::REF_TYPE]);
    }
}